// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Author: wsfuyibing <682805@qq.com>
// Date: 2024-11-19

package zipkin_plugin

import (
	"encoding/base64"
	"encoding/binary"
	"encoding/json"
	"fmt"
	"gitee.com/go-libs/log/common"
	"gitee.com/go-libs/log/conf"
	"gitee.com/go-libs/log/tracer/zipkin_plugin/model"
	"github.com/valyala/fasthttp"
	"net/http"
	"time"
)

// 上报工具.
// 上报链路跟踪数据到 Jaeger 服务端.
type reporter struct {
	timeout time.Duration
}

// +------------------------------------------------------------------------------+
// | Interface methods                                                            |
// +------------------------------------------------------------------------------+

func (o *reporter) Report(_ common.SpanFormatter, spans []common.Span) (err error) {
	var (
		cfg  = conf.New()
		data []byte
		list = make([]*model.SpanModel, 0)

		request  = fasthttp.AcquireRequest()
		response = fasthttp.AcquireResponse()
	)

	defer func() {
		fasthttp.ReleaseRequest(request)
		fasthttp.ReleaseResponse(response)
	}()

	for _, span := range spans {
		if v := o.buildZipkinSpan(cfg, span); v != nil {
			list = append(list, v)
		}
	}

	if len(list) > 0 {
		if data, err = json.Marshal(list); err != nil {
			return
		}
	}

	// Prepare request.
	request.SetRequestURI(cfg.ZipkinTracer.Endpoint)
	request.SetBody(data)
	request.Header.SetMethod(http.MethodPost)
	request.Header.SetContentType("application/x-thrift")

	// BasicAuth
	if usr := cfg.ZipkinTracer.Username; usr != "" {
		pwd := cfg.ZipkinTracer.Password
		request.Header.Set("Authorization", fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(usr+":"+pwd))))
	}

	// Send http request.
	if err = fasthttp.DoTimeout(request, response, o.timeout); err == nil {
		if code := response.StatusCode(); code != http.StatusOK && code != http.StatusAccepted {
			err = fmt.Errorf(`HTTP %d %s`, code, http.StatusText(code))
		}
	}
	return
}

// +------------------------------------------------------------------------------+
// | Access methods                                                               |
// +------------------------------------------------------------------------------+

func (o *reporter) buildZipkinSpan(cfg *conf.Config, span common.Span) (m *model.SpanModel) {
	var (
		sid = span.GetTracing().GetSpanId()
		tid = span.GetTracing().GetTraceId()
	)

	m = &model.SpanModel{
		SpanContext: model.SpanContext{
			TraceID: model.TraceID{
				High: binary.BigEndian.Uint64(tid.Byte()[:8]),
				Low:  binary.BigEndian.Uint64(tid.Byte()[8:]),
			},
			ID: model.ID(binary.BigEndian.Uint64(sid.Byte())),
		},
		Name:          span.GetName(),
		Kind:          model.Client,
		Timestamp:     span.GetStartTime(),
		Duration:      span.GetDuration(),
		Shared:        false,
		LocalEndpoint: &model.Endpoint{ServiceName: cfg.ServiceName},
		Annotations:   make([]model.Annotation, 0),
		Tags:          make(map[string]string),
	}

	if v := span.GetTracing().GetParentSpanId(); v != nil {
		x := model.ID(binary.BigEndian.Uint64(v.Byte()))
		m.SpanContext.ParentID = &x
	}

	m.LocalEndpoint = &model.Endpoint{
		ServiceName: cfg.ServiceName,
	}

	for k, v := range map[string]any{
		"service.addr":    cfg.ServiceAddresses,
		"service.pid":     cfg.Pid,
		"service.port":    cfg.ServicePort,
		"service.name":    cfg.ServiceName,
		"service.version": cfg.ServiceVersion,
	} {
		m.Tags[k] = fmt.Sprintf(`%v`, v)
	}

	// Tag user.
	for k, v := range span.GetAttr() {
		m.Tags[k] = fmt.Sprintf(`%v`, v)
	}

	// Annotations.
	for _, log := range span.GetLogs() {
		m.Annotations = append(m.Annotations, model.Annotation{
			Timestamp: log.Time(),
			Value:     fmt.Sprintf(`[%s] %s`, log.Level(), log.Message()),
		})
	}
	return
}

func (o *reporter) init() *reporter {
	if n := conf.New().JaegerTracer.Timeout; n > 0 {
		o.timeout = time.Duration(n) * time.Second
	} else {
		o.timeout = time.Second * 5
	}
	return o
}
